/* @flow */

import Dep from './dep'
import VNode from '../vdom/vnode'
import { arrayMethods } from './array'
import {
  def,
  warn,
  hasOwn,
  hasProto,
  isObject,
  isPlainObject,
  isPrimitive,
  isUndef,
  isValidArrayIndex,
  isServerRendering
} from '../util/index'

// 所有定义在 arrayMethods 对象上的 key即数组变异方法的名字(如push, unshfit,shift, pop,等)
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)

/**
 * 数据观测的开关：
 */
export let shouldObserve: boolean = true
// 修改开关的状态
export function toggleObserving (value: boolean) {
  shouldObserve = value
}

/**
 * Observer构造函数负责将数据对象转换为响应式数据
 * value： 数据对象
 * dep:
 * vmCount:
 */
export class Observer {
  // 定义三个实例属性
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that have this object as root $data

  constructor (value: any) {
    // vaue属性保存传递的数据对象
    this.value = value
    // 实例对象的 dep 属性，保存了一个新创建的 Dep 实例对象(Dep: 用于收藏 依赖信息)
    this.dep = new Dep()
    // 实例对象的 vmCount 属性被设置为 0
    this.vmCount = 0
    // 在value数据对象上定义__ob__属性(不可枚举,在数据遍历时不会被遍历)，且属性值为当前 Observer 实例对象
    /**
     * 数据对象的__ob__属性如下：
     * __ob__:{
     *  value: 父级元素, // value 属性指向 data 数据对象本身，这是一个循环引用
     *  dep: dep实例对象, // new Dep()， 包含 Dep 实例对象
     *  vmCount: 0
     * }
     */
    def(value, '__ob__', this)
    // 区分数据对象到底是数组还是一个纯对象
    // 数据结构的处理：检测被观测的值 value 是否是数组(观察数组需在监听push、pop、shift、unshift、splice、sort 以及 reverse 等方法)
    if (Array.isArray(value)) {
      // 先监测环境是否支持__proto__属性，数组实例的 __proto__ 属性指向的就是数组构造函数的原型，即 arr.__proto__ === Array.prototype 为真
      // __proto__属性在IE11+才开始被支持，去低版本浏览器，需添加兼容处理。
      if (hasProto) {
        // 设置数组实例的 __proto__ 属性，让其指向一个代理原型，从而做到拦截
        protoAugment(value, arrayMethods)
      } else {
        // 不支持__proto__属性时：做兼容处理
        copyAugment(value, arrayMethods, arrayKeys)
      }
      // 以该数组实例作为参数调用了 Observer 实例对象的 observeArray 方法
      this.observeArray(value)
    // 纯数据对象的处理  
    } else {
      this.walk(value)
    }
  }

  /**
   * 实例方法 walk
   * 接收对象数据
   */
  walk (obj: Object) {
    // 使用 Object.keys(obj) 获取对象所有可枚举的属性
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      // obj对象定义一个响应式的属性(即为每个key设置一对getter/setter)，传递的参数为(数据对象, 属性的键名)
      // 每次调用 defineReactive 定义访问器属性时，该属性的 setter/getter 都闭包引用了一个属于自己的“筐”:即dep
      // 每个字段的 Dep 对象都被用来收集那些属于对应字段的依赖，此时并没有传第三个参数
      defineReactive(obj, keys[i])
    }
  }

  /**
   * 实例方法 observeArray
   * 接收数组数据
   */
  observeArray (items: Array<any>) {
    // 为了使嵌套的数组或对象同样是响应式数据，我们需要递归的观测那些类型为数组或对象的数组元
    // 如多级结构：[{a:1,b:1},{a:2,b:2}] 这种：增加或删除会监测到，但如果修改元素对象的a的值就会观察不到，所以要递归的观测数组元
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}

// helpers

/**
 * 设置数组实例的 __proto__ 属性，让其指向一个代理原型，从而做到拦截
 */
function protoAugment (target, src: Object) {
  /* eslint-disable no-proto */
  target.__proto__ = src
  /* eslint-enable no-proto */
}

/**
 * Augment a target Object or Array by defining
 * keys：定义在 arrayMethods 对象上的所有函数的键(也即：所有要拦截的数组变异方法的名称)
 */
/* istanbul ignore next */
function copyAugment (target: Object, src: Object, keys: Array<string>) {
  for (let i = 0, l = keys.length; i < l; i++) {
    const key = keys[i]
    // 使用 def 函数在数组实例上定义与数组变异方法同名的且不可枚举的函数，实现拦截操作
    def(target, key, src[key])
  }
}

/**
 * value: 要观测的数据
 * asRootData: 布尔值，代表将要被观测的数据是否是根级数据
 */
export function observe (value: any, asRootData: ?boolean): Observer | void {
  // 判断如果要观测的数据不是一个对象或者是 VNode 实例，则直接 return
  // isObject：判断一个非null的元素是不是对象，typeof(null,[],{})的结果均为object
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  // 定义变量 ob，该变量用来保存 Observer 实例
  let ob: Observer | void
  // 用 hasOwn 函数检测数据对象 value 自身是否含有 __ob__ 属性，并且 __ob__ 属性应该是 Observer 的实例(为真则说明该对象已经被观测过)
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    // 则直接将数据对象自身的 __ob__ 属性的值作为 ob 的值(原因是：当一个数据对象被观测之后将会在该对象上定义 __ob__ 属性，
    // 所以 if 分支的作用是用来避免重复观测一个数据对象)
    ob = value.__ob__
  //else 说明该对象没有被观测过
  } else if (
    // 条件1、shouldObserve 必须为 true(shouldObserve 变量也定义在 core/observer/index.js)
    shouldObserve &&
    // 条件2、只有当非服务端渲染才会观测数据，isServerRendering()用来判断是否是服务端渲染，
    !isServerRendering() &&
    // 条件3、只有当数据对象是数组或纯对象的时候，才有必要对其进行观测
    (Array.isArray(value) || isPlainObject(value)) &&
    // 条件4、要被观测的数据对象必须是可扩展的(Object.preventExtensions()、Object.freeze() 以及 Object.seal()三种方法会使的对象不可扩展)
    Object.isExtensible(value) &&
    // 条件5、Vue 实例对象拥有 _isVue(_init初始化时被添加) 属性，所以这个条件用来避免 Vue 实例对象被观测
    !value._isVue
  ) {
    // 执行 ob = new Observer(value) 对数据对象进行观测，即通过Observer构造函数将数据对象转换为响应式数据。
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  // 最终将Observer 实例返回
  /**
   * const data = {a: { b: 1}}
   * 调用 observe(data) 处理后， data对象变为：
   * const data = {
   *  // 属性 a 通过 setter/getter 通过闭包引用着 dep 和 childOb
   *  a: {
   *   // 属性 b 通过 setter/getter 通过闭包引用着 dep 和 childOb
   *   b: 1,
   *   __ob__: {a, dep, vmCount}
   *  },
   *  __ob__: {data, dep, vmCount} 
   * }
   */
  return ob
}

/**
 * 为obj对象定义一个响应式的属性(该属性有get和set方法)
 */
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  // 先定义dep常量
  const dep = new Dep()
  // 通过 Object.getOwnPropertyDescriptor 函数获取该字段可能已有的属性描述对象，并将该对象保存在 property 常量中
  const property = Object.getOwnPropertyDescriptor(obj, key)
  // 如果该对象存在 且 不可配置则直接返回(不可配置的属性无法改变属性值)
  if (property && property.configurable === false) {
    return
  }

  // 保存了来自 property 对象的 get 和 set 函数(有可能其get和set函数已经存在，所以先将其缓存，防止后面被覆盖 )
  const getter = property && property.get
  const setter = property && property.set
  // (!getter || setter) 为边界条件处理：当属性原本存在 getter 时，是不会触发取值动作，而值在walk函数调用时又没有传，这样会导致val为undefined
  // 且用户自定义的get函数可能引发不可预见行为，
  // 如果属性原本只有get,没有set，该属性默认不会被深度观察，但经过defineReactive 函数的处理之后被重新定义了getter 和 setter，这会导致本不会
  // 被深度观察的属性在重新赋值后，新的值却被观察了，使的定义响应式数据时行为的不一致
  // 所以采用的方案为：当属性拥有原本的 setter 时，即使拥有 getter 也要获取属性值并观测之
  if ((!getter || setter) && arguments.length === 2) {
    // 不传值时通过key去获取，但获取的结果可能是undefined
    val = obj[key]
  }
  // defineReactive的最后一个参数shallow为真时表示val也是一个对象，此时应该继续调用 observe(val) 函数观测该对象从而深度观测数据对象，
  // 由于在walk中调用defineReactive方法时未传递第三个参数，所以此时shallow为undefined,即默认就是深度观测
  // childOb：为继续调用 observe(val) 函数观测该对象而获得的深度观测数据对象
  // 如果 val 是 undefined，不会深度观测
  let childOb = !shallow && observe(val)
  // 用 Object.defineProperty 函数定义访问器属性
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    // get函数： 要能够正确地返回属性的值才行(做了2件事： 1、收集依赖，2、正确地返回属性值) 
    get: function reactiveGetter () {
      // getter 常量中保存的是属性原有的 get 函数，如果已经有get函数了，则直接调用并将返回结果作为属性的值
      const value = getter ? getter.call(obj) : val
      // 收集依赖：Dep.target 中保存的值就是要被收集的依赖(观察者)
      if (Dep.target) {
        // 闭包引用了上面的 dep 常量(每一个数据字段都通过闭包引用着属于自己的 dep 常量)：此处可简单理解为收集依赖
        dep.depend()
        // 闭包也引用了childOb，childOb === obj.key.__ob__; 而childOb.dep === data.key.__ob__.dep
        if (childOb) {
          // 这一步说明：除了要将依赖收集到属性 key 自己的“筐”里之外, 还要将同样的依赖收集到 data.key.__ob__.dep 这个”筐“里
          // 同样的依赖放置在2个框中的原因是：两个”筐“里收集的依赖的触发时机不同
          // 第一个”筐“是 dep；第二个”筐“是 childOb.dep
          // 第一个”筐“里收集的依赖的触发时机是当属性值被修改时触发，即在 set 函数中触发：dep.notify()
          // 第二个”筐“里收集的依赖的触发时机是在使用 $set 或 Vue.set 给数据对象添加新属性时触发
          // __ob__ 属性以及 __ob__.dep 的主要作用是为了添加、删除属性时有能力触发依赖，而这也是 Vue.set 或 Vue.delete 的原理
          childOb.dep.depend()
          // 如果读取的属性值是数组，则调用dependArray 函数逐个触发数组每个元素的依赖收集
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    // set函数：触发依赖的机制？(做了2件事： 1、正确地为属性设置新值，2、触发相应的依赖) 
    set: function reactiveSetter (newVal) {
      // 取得属性原有的值
      const value = getter ? getter.call(obj) : val
      /* 新值与新值自身都不全等，同时旧值与旧值自身也不全等时也直接返回(排除NaN，因为 NaN === NaN 为false) */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* 在非生产环境下执行 customSetter 函数,customSetter 函数是 defineReactive 函数的第四个参数 */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      // #7981: 访问器属性没有setter，只有getter
      if (getter && !setter) return
      // setter 常量存储的是属性原有的 set 函数，即如果属性原来拥有自身的 set 函数，那么应该继续使用该函数来设置属性的值，从而保证属性原有的设置操作不受影响
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      // 如果属性被设置了新值且新值为对象或数组，则此时的新对象或新数组并未被观察，因些须对新值进行观测，同时使用新的观测对象重写 childOb 的值
      childOb = !shallow && observe(newVal)
      // 闭包引用了上面的 dep 常量
      dep.notify()
    }
  })
}

/**
 * set函数的实现体：
 * target：要被添加属性的对象
 * key: 添加的属性的键名
 * val：添加的属性的值
 */
export function set (target: Array<any> | Object, key: any, val: any): any {
  // 如果 set 函数的第一个参数是 undefined 或 null 或者是原始类型值，那么在非生产环境下会打印警告信息
  // 理论上只能为对象(或数组)添加属性(或元素)
  if (process.env.NODE_ENV !== 'production' &&
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
  }
  // target为数组，且 key 是有效的数组索引(大于0且有有效值的整数)
  // this.arr[0] = 1 不会触发响应，但this.$set(this.arr,0,1)会触发响应
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key)
    // 将指定位置元素的值替换为新值，数组元素的splice方法会被拦截并观察到，所以此方法会触发响应监测
    target.splice(key, 1, val)
    return val
  }
  // target为对象, key 在 target 对象上，或在 target 的原型链上，同时必须不能在 Object.prototype 上
  if (key in target && !(key in Object.prototype)) {
    target[key] = val
    return val
  }
  const ob = (target: any).__ob__
  // _isVue存在说明正在使用 Vue.set/$set 函数为 Vue 实例对象添加属性，而Vue.set/$set 函数不允许这么做，非生产环境下直接给出警告
  // 被观测的数据对象是否是根数据对象，ob.vmCount即target.__ob__.vmCount，在观察对象为data对象时，ob.vmCount会++
  // 所以，当ob.vmCount>0时表示 的是要为根对象添加属性，在vue中是不允许的，如：通过vue.set或$set为data对象添加新属性会直接给出警告
  // 这是因为根对象不是响应式对象，如vue中的要对象：data, wtach, methods等，根对象的内容可以是响应式的
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== 'production' && warn(
      'Avoid adding reactive properties to a Vue instance or its root $data ' +
      'at runtime - declare it upfront in the data option.'
    )
    return val
  }
  // 将target.__ob__不存在时，直接赋值(不存在)
  if (!ob) {
    target[key] = val
    return val
  }
  // 用 defineReactive 函数设置属性值，保证新加的属性值也是响应式的
  defineReactive(ob.value, key, val)
  // 触发响应
  ob.dep.notify()
  return val
}

/**
 * del 函数实现:
 * target: 要被删除属性的对象
 * key: 要删除属性的对象上的键名 key
 * 
 */
export function del (target: Array<any> | Object, key: any) {
  // 非生产环境下，target为原始类型或undefined,null时给出警告
  if (process.env.NODE_ENV !== 'production' &&
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(`Cannot delete reactive property on undefined, null, or primitive value: ${(target: any)}`)
  }
  // target为对象时，且key为有效索引时，直接调用splice方法触发观察
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.splice(key, 1)
    return
  }
  const ob = (target: any).__ob__
  // 根对象时不支持删除操作
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== 'production' && warn(
      'Avoid deleting properties on a Vue instance or its root $data ' +
      '- just set it to null.'
    )
    return
  }
  // key不在target上时，直接返回
  if (!hasOwn(target, key)) {
    return
  }
  // 经过上述处理后，直接执行删除操作
  delete target[key]
  // 判断 ob 对象是否存在，如果不存在说明 target 对象原本就不是响应的，所以直接返回(return)即可 
  if (!ob) {
    return
  }
  // ob 对象是响应式对象时，触发响应执行
  ob.dep.notify()
}

/**
 * Collect dependencies on array elements when the array is touched, since
 * we cannot intercept array element access like property getters.
 */
function dependArray (value: Array<any>) {
  for (let e, i = 0, l = value.length; i < l; i++) {
    e = value[i]
    // 如果该元素的值拥有 __ob__ 对象和 __ob__.dep 对象，说明该元素也是对象或数组，此时通过e.__ob__.dep.depend()来收集依赖
    e && e.__ob__ && e.__ob__.dep.depend()
    // 如果该元素是数组，需要递归调用 dependArray 继续收集依赖
    // 这么做的原因是因为：数组的索引是非响应式的，如arr[0] = 3 不会触发响应，因为数组的索引不是'访问器属性'
    if (Array.isArray(e)) {
      dependArray(e)
    }
  }
}
